前言
为什么需要做服务器jvm自动发现的监控呢?这个事情主要有两点原因:
- zabbix默认监控jvm状态是使用jmx中转进行监控的,监控效率比较低下
- zabbix使用jmx监控jvm的时候由于一个主机上的键值不能重复,也就导致了一台主机上只能监控一个jvm实例
以上两点原因导致zabbix通过jmx监控jvm的实现不是很理想,加上最近老大要求收集服务器上面跑的所有java应用的信息,于是自己琢磨了下,还是自己动手,丰衣足食。利用了周末的时间,通过使用shell脚本+java工具jstat+zabbix实现监控主机上多jvm实例的功能。
一、概念的理解
首先,既然要监控jvm状态,那就必须要了解jvm里面的信息,楼主通过搜索资料加自动脑补,把网上的资料取其精华,去其糟粕,整理了一下。JVM中的内存分类分为堆内存和非堆内存,堆内存是给实际应用使用的,非堆内存是给jvm容器使用的。我们主要关心的是堆内存这块。在堆内存里面,给内存分为如下几块:
- Young代(年轻代)
- Old代(老年代)
- Perm代(永久代)(关于这一点,在JDK7和JDK8中情况不一样,将在后面进行分析)
其中,年轻代里面又分成了三块,如下:
- Eden代(伊甸园代)
- survivor0代(0号幸存区)
- survivor1代(1号幸存区)
至于更详细的关于JVM堆内存的信息,各位可以自行百度或者google,我这里就不赘述了,毕竟我也是个半桶水,自己找了点资料外加脑补到的一些东西,不敢在关公门前耍大刀了。
当然,还得科普一个东西,那就是GC,所谓的GC就是JVM在运行的时候会有一个垃圾回收机制,这个垃圾回收机制是什么情况呢?就是在程序运行的时候会产生很多已经不使用的空间,但还是被占用了的情况,这样会造成很多不必要的浪费,于是JVM就有一个垃圾回收机制,针对程序中已经不使用的内存资源,会进行回收释放,这个过程就叫做GC。当然,关于GC还有很多内容我这里也没有详述,理由同上条。各位看官只需要知道GC是JVM监控里面的一个很重要的参数就行了。
二、JAVA工具的选用
java工具有很多,关于jvm监控的工具主要有如下几个:
- jstat
- jmap
- jstack
其中jmap –heap pid可以抓出挺多的关于某个jvm的运行参数,但是老大提醒我最好不要使用jmap进行jvm监控,具体没有说明原因。于是本着打破砂锅问到底的精神,我又去搜了一把,发现了如下内容:
jmap最主要的危险操作是下面这三种:
- jmap -dump
这个命令执行,JVM会将整个heap的信息dump写入到一个文件,heap如果比较大的话,就会导致这个过程比较耗时,并且执行的过程中为了保证dump的信息是可靠的,所以会暂停应用。
- jmap -permstat
这个命令执行,JVM会去统计perm区的状况,这整个过程也会比较的耗时,并且同样也会暂停应用。
- jmap -histo:live
这个命令执行,JVM会先触发gc,然后再统计信息。
上面的这三个操作都将对应用的执行产生影响,所以建议如果不是很有必要的话,不要去执行。
所以,从上面三点来看,jmap命令对jvm状态影响还是比较大的,而且执行jmap –heap的时间也比较长,效率较低,予以排除。
接下来是jstack,这个命令可以深入到JVM里面对JVM运行问题进行排查,据说还可以统计JVM里面的线程数量。但是这个命令执行效率也比较低,被排除掉了。
于是剩下的只有一个jstat命令了。下面来详细的讲解该命令的使用了,咳咳,各位快点打起点精神来,这可是重头戏来了。
首先,列出jstat命令的一些使用案例吧:
|
|
可以看出上面我列出的命令执行结果为什么有两行呢,这是因为是用不同的jdk版本执行的。
上面是JDK7执行结果,下面是JDK8执行结果,这两个版本之间输出的结果是有差距的,下面,就来分析为什么会产生这种差异。
JDK7和JDK8中JVM堆内存划分差异
如果记性好的童鞋们应该还能记得我上面在介绍JVM堆内存分类的时候括号里写的那个东东吧,没错,就是这个东西导致的。在JDK7中的Perm代(永久代)在JDK8中被废除了,取而代之的是Metadata代(元数据代),据说这个元数据代相对于永久代进行了优化,如果不设置最大值的话,默认会按需增长, 不会造成像Perm代中内存占满后会爆出内存溢出的错误,元数据代也可以设置最大值,这样的话,当内存区域被消耗完的时候将会和Perm代一样爆出内存溢出的错误。(PS:原谅我的班门弄斧,只能解释到这一个层面了。)
好了,解释清楚了JDK7和JDK8的差异以后,接下来我们来解释jstat抓到的这些参数了。
|
|
好了,上面就是我找到的一些对jstat获取的数据意思的统计,各位看官可以做个参考。
好了,这一章的内容到此基本结束,前面的东西都是一些理论类的东西,没有实际的操作。俗话说,光说不练假把式。接下来,我们将开启下一章的旅程,脚本+jstat的使用。
三、脚本+jstat获取数据
首先,我们来看一下该章节介绍的几个脚本吧:
jvm_list.sh 获取该机器上所有运行的JVM的进程对应的程序根目录以及程序名称
get_jvmlist.sh 将获取的该机器上的所有进程对应的程序名称序列化成json格式并发送给zabbix服务器
get_jvmstatus.sh 通过获取的程序根目录获取到对应的程序进程,再通过jstat抓取数据写入到文件中缓存
set_jvmstatus.sh zabbix通过调用该脚本获取缓存文件中的关于某个JVM进程的状态信息
简单介绍了上面几个脚本的功能,下面我们列出这几个脚本的实际内容:
|
|
该脚本的目的是先通过使用ps -fC java命令获取该机器上面除了flume进程外的所有其他java进程(我这边使用的是flume来收集业务日志的。)
然后,通过获取到的PID使用ll /proc/pid/cwd命令获取该进程的程序根目录,后面那些判断是获取该进程对应的包名(这一步各位可以根据自己公司的情况自行修改,我这边取包名的方式并不能够匹配各位公司的设置,在下爱莫能助了。)
最后是将获取到的程序根目录和包名存放在变量packagePath对应的文件中。
|
|
这个脚本的作用就是通过读取文件里面的包名,然后将包名进行json序列化输出,没什么好讲的,套路套一个循环脚本就行。
接下来就是重要的脚本了,调用jstat获取JVM状态,并缓存到文件中。
|
|
这里面首先是通过获取到程序的根目录,然后我这的java程序除了tomcat跑的之外,其他的java程序都是通过Main.class启动的,所以可以获取到AppName,这样通过ps命令就能找到其对应的PID了,而如果是tomcat启动的进程的话,在程序根目录下面的tomcat目录下有一个tomcat.pid文件里面有该程序的PID。后面被注释的那一端代码其实之前是加上去的,那段代码的作用是判断该进程使用的是JDK7还是JDK8启动的,当初的计划是想着如果是JDK7启动的进程就用JDK7的jstat去获取数据,如果是JDK8启动的进程就用JDK8的jstat去获取数据,后来发现不同版本的JDK获取的数据格式不同,于是。。。。。。后悔莫及的把那段代码注释掉了。后面综合公司实际情况考虑,JDK8的程序用得比较多,JDK7的程序相对来说比较少,并且慢慢都会向JDK8进行转换,所以,权衡利弊之下,之后将jstat的JDK全部换成了JDK8,这样的影响就是获取不到JDK7的永久代数据。当然,各位有兴趣的话,也可以JDK7和JDK8同时使用,在过滤输出文件的时候加一个标志位进行判断,当然,我这里暂时没有做这方面的修改。。。毕竟时间有限。。。
第四个脚本
|
|
这套脚本没什么讲的,就是重复的进行一些判断,抓数据并输出(注意,之前写的获取的jstat参数的值其实是不准确的,获取的值是以KB为单位而不是以字节为单位,所以我取完数据后对数据进行成字节为单位了。)
接下来,讲一下这几个脚本该怎么部署。我这里的zabbix_agentd是通过yum安装的,所以安装在/usr/local目录下,配置文件在/usr/local/etc目录下,需要在zabbix_agentd.conf里面添加下面两行获取数据的key(注意,添加好后一定要记得重启zabbix_agentd进程):
|
|
然后脚本都放置在/usr/local/etc/scripts/目录下,该目录下的脚本权限如下:
|
|
然后需要在crontab里面定义jvm_list.sh和get_jvmstatus.sh脚本的定时任务,我这里定义的如下:
|
|
注意这两个脚本必须要以root权限去执行,因为里面涉及到的一些命令只有root用户才有权限去执行。
之后可以手动执行脚本去获取数据,看是否能够抓取到相应的数据。
四、zabbix获取数据
通过之前的脚本部署,可以在zabbix_server上面通过zabbix_get命令去检查是否获取到了相应的数据:
|
|
这里可以获取到数据了(注意IP被注释掉了,为了保护隐私哈,包名也被刻意修改了)
接下来就可以部署模板了,至于模板已经做好了,可以直接在附件里面下载。至于模板我制作了一些简单的key的值收集,以及图像的展示,至于监控报警值的设置,由于各个公司的环境不一样,需要各位自己根据自己需求自行设置。